// Copyright 2010, Vanya Davidenko.
// Используемая кодировка: UTF-8.

#ifndef SCRW_OPENSSL_INIT_H_
#define SCRW_OPENSSL_INIT_H_

#include <assert.h>
#include <stdexcept>

#include <libv/lock_spin.h>
#include <libv/thread_id.h>
#include <libv/uncopyable.h>

#include <openssl/crypto.h>

// Если не определен макрос — определяем, а после include возвращаем как было.
#ifndef LOKI_CLASS_LEVEL_THREADING
# define LOKI_CLASS_LEVEL_THREADING
# define UNDEF_LOKI_CLASS_LEVEL_THREADING
#endif
#include <loki/Threads.h>
#include <loki/Singleton.h>
#ifdef UNDEF_LOKI_CLASS_LEVEL_THREADING
# undef LOKI_CLASS_LEVEL_THREADING
# undef UNDEF_LOKI_CLASS_LEVEL_THREADING
#endif



/**@file About:
  * http://www.openssl.org/docs/crypto/threads.html
  * http://old.nabble.com/When-to-use-CRYPTO_set_locking_callback()-and-CRYPTO_set_id_callback()--td5849882.html */


/** Структура, используемая в качестве объекта синхронизации в OpenSSL.
  * Объявлена глобально, так как OpenSSL написан на С и ожидает эту структуру
  * в глобальном пространстве имен. */
struct CRYPTO_dynlock_value
{
    CRYPTO_dynlock_value() : m() {}
    libv::ReadWriteSpin m;
};



namespace scrw
{

/** RAII-объект, инициализирующий OpenSSL в конструкторе
  * и очищающий в деструкторе.
  * Так как создавать объект нужно один раз, рекомендуется использование
  * класса-одиночки (синглтона) TheOpenSSLInitializationAndCleanUp, в таком
  * случае первый вызов TheOpenSSLInitializationAndCleanUp::Instance() проведет
  * инициализацию, а последующие будут проигнорированы. */
class OpensslInitializationAndCleanUp : libv::Uncopyable
{
  public:
    OpensslInitializationAndCleanUp()
    {
        assert(locks_ == NULL);

        // OpenSSL написан на C, потому нельзя допустить генерацию исключения.
        try
        {
            locks_ = new CRYPTO_dynlock_value[CRYPTO_num_locks()];
        }
        catch(::std::bad_alloc&)
        {
            locks_ = NULL;
        }

        CRYPTO_set_locking_callback(LockingFunction);
        CRYPTO_THREADID_set_callback(ThreadIDFunction);
        CRYPTO_set_dynlock_create_callback(DynCreateFunction);
        CRYPTO_set_dynlock_lock_callback(DynLockFunction);
        CRYPTO_set_dynlock_destroy_callback(DynDestroyFunction);
    }


    ~OpensslInitializationAndCleanUp()
    {
        assert(locks_ != NULL);

        CRYPTO_set_locking_callback(NULL);
        CRYPTO_THREADID_set_callback(NULL);
        CRYPTO_set_dynlock_create_callback(NULL);
        CRYPTO_set_dynlock_lock_callback(NULL);
        CRYPTO_set_dynlock_destroy_callback(NULL);

        delete[] locks_;
        locks_ = NULL;
    }


    /** Использования объекта синхронизации в заданном режиме.
      * @param mode тип блокировки (взятие или освобождение)
      * @param l указатель на объект синхронизации. */
    static void Lock(const int mode, CRYPTO_dynlock_value* l)
    {
        if ( mode & CRYPTO_LOCK )
        {
            if ( mode & CRYPTO_WRITE )
            {
                l->m.WriteLock();
            }
            else
            {
                assert(mode & CRYPTO_READ);
                l->m.ReadLock();
            }
        }
        else
        {
            assert(mode & CRYPTO_UNLOCK);

            if ( mode & CRYPTO_WRITE )
            {
                l->m.WriteUnlock();
            }
            else
            {
                assert(mode & CRYPTO_READ);
                l->m.ReadUnlock();
            }
        }
    }


    /** Функция обратного вызова, используемая OpenSSL,
      * для использования объекта синхронизации.
      * @param mode тип блокировки (взятие или освобождение)
      * @param n номер блокировки в глобальном массиве locks_
      * @param file имя файла, в котором вызвана функция (для отладки)
      * @param line строка файла file, в котором вызвана функция. */
    static void LockingFunction(int mode, int n,
                                const char* /* file */, int /* line */)
    {
        Lock(mode, &locks_[n]);
    }


    /** Функция обратного вызова, используемая OpenSSL,
      * для получения идентификатора текущего потока. */
    static void ThreadIDFunction(CRYPTO_THREADID *id)
    {
        CRYPTO_THREADID_set_numeric(id, libv::thread_id());
    }


    /** Функция обратного вызова, используемая OpenSSL,
      * для создания объекта синхронизации.
      * @param file имя файла, в котором вызвана функция (для отладки)
      * @param line строка файла file, в котором вызвана функция.
      * @return указатель на созданный объект синхронизации. */
    static CRYPTO_dynlock_value* DynCreateFunction(const char* /* file */,
                                                   int /* line */)
    {
        // OpenSSL написан на C, потому нельзя допустить генерацию исключения.
        try
        {
            CRYPTO_dynlock_value* result = new CRYPTO_dynlock_value;
            return result;
        }
        catch(::std::bad_alloc&)
        {
            return NULL;
        }
    }


    /** Функция обратного вызова, используемая OpenSSL,
      * для уничтожения объекта синхронизации.
      * @param l указатель на объект синхронизации
      * @param file имя файла, в котором вызвана функция (для отладки)
      * @param line строка файла file, в котором вызвана функция. */
    static void DynDestroyFunction(CRYPTO_dynlock_value* l,
                                   const char* /* file */, int /* line */)
    {
        delete l;
    }


    /** Функция обратного вызова, используемая OpenSSL,
      * для использования объекта синхронизации.
      * @param mode тип блокировки (взятие или освобождение)
      * @param l указатель на объект синхронизации
      * @param file имя файла, в котором вызвана функция (для отладки)
      * @param line строка файла file, в котором вызвана функция. */
    static void DynLockFunction(int mode, CRYPTO_dynlock_value* l,
                                const char* /* file */, int /* line */)
    {
        Lock(mode, l);
    }

  private:
    static CRYPTO_dynlock_value* locks_;
};


typedef Loki::SingletonHolder<
    OpensslInitializationAndCleanUp,
    Loki::CreateUsingNew,
    Loki::DefaultLifetime,
    Loki::ClassLevelLockable> TheOpenSSLInitializationAndCleanUp;

}  // scrw

#endif  // SCRW_OPENSSL_INIT_H_
